Object-oriented programming

Our first example is an example of composition. We create a Company that consists a list of Persons and an account balance.

Then after we structure our classes with desired behaviour we can use them quite freely.

Create a class called Person for storing the following information about a person:

  • name

Create a method say_hi that returns the string "Hi, I'm " + the person's name.


In [ ]:
class Person(object):
    def __init__(self, name):
        self.name = name
        
    def say_hi(self):
        return "Hi, I'm " + self.name

Run the following code to test that you have created the person correctly:


In [ ]:
persons = []
joe = Person("Joe")
jane = Person("Jane")
persons.append(joe)
persons.append(jane)

Now create a class Employee that inherits the class Person. In addition to a name, Employees have a title (string), salary (number) and an account_balance (number).

Override the say_hi method to say "Hi I'm " + name + " and i work as a " + title


In [ ]:
# the reference to Person on the line below means that the object inherits 
# Employee
class Employee(Person):
    def __init__(self, 
                 name, 
                 salary, 
                 title="Software Specialist", 
                 account_balance=0):
        #this calls the constructor of Person class
        super().__init__(name)
        self.salary = salary
        self.title = title
        self.account_balance = account_balance
        
    def say_hi(self):
        return "Hi I'm " + self.name + " and I work as a " + self.title

Every employee is also a person.


In [ ]:
persons = []
joe = Person("Joe")
jane = Person("Jane")
persons.append(joe)
persons.append(jane)
emp1 = Employee("Jack", 3000)
emp2 = Employee("Jill", 3000)
persons.append(emp1)
persons.append(emp2)
for person in persons:
    print(person.say_hi())

Now create a class called Company, which has a name and a list of Employee objects called employees and an account balance for the company.

Make a method payday(self) that will go through the list of employees and deduct their salary from the corporate account and add it to the employee account. Before you start deducting money compute the sum of salaries and make sure it is higher than the account balance. If it is not, raise an instance of the NotEnoughMoneyError.

Make a method layoff(self) that will remove one employe from the list of employees. If there are no more employees raise a NoMoreEmployeesException.


In [ ]:
class NotEnoughMoneyError(Exception):
    pass

class NoMoreEmployeesError(Exception):
    pass

class Company(object):
    def __init__(self, title, employees = [], account_balance=0):
        self.title = title
        self.employees = employees
        self.account_balance = account_balance
        
    def _has_money_to_pay(self):
        counter = 0
        for employee in self.employees:
            counter += employee.salary
        return counter < self.account_balance
    
    def payday(self):
        if self._has_money_to_pay():
            for employee in self.employees:
                employee.account_balance += employee.salary
                self.account_balance -= employee.salary
        else:
            raise NotEnoughMoneyError("not enough money to pay")
    
    def layoff(self):
        if self.employees:
            self.employees.pop() # just pop the last one, logic wasn't specified
        else:
            raise NoMoreEmployeesException

Okay, you've worked this far just creating the model, let's put it to use.

Make a method smart_payday(company). The method should attempt to call the payday method of the company. If the call raises a NotEnoughMoneyException lay off a worker and then try again. Don't catch the NoMoreEmployeesException as that should be handled at a higher level.


In [ ]:
def smart_payday(company):
    payment_succeeded = False
    while not payment_succeeded:
        try:
            company.payday()
            payment_succeeded = True
        except NotEnoughMoneyError:
            company.layoff()

In [ ]:
# a bit of test code
names_and_salaries = [
    ("Jane", 3000), 
    ("Joe", 2000),
    ("Jill", 2000),
    ("Jack", 1500)
]

workers = [Employee(name, salary) \
           for name, salary in names_and_salaries]
scs = Company("SCS", employees=workers, account_balance=12000)

smart_payday(scs)
print(scs.account_balance)
print(len(scs.employees))
smart_payday(scs)
print(scs.account_balance)
print(len(scs.employees))
print(scs.employees)

Observe how printing the employees list is not very informative? Adding a magic method called __repr__ will help with that.

Extra: Magic methods

Create a class called element for storing the following data

  • name
  • symbol
  • atomic number
  • molecular weight

You can use the following as example data.

Element symbol atomic number molecular weight
Hydrogen H 1 1.01
Iron Fe 26 55.85
Silver Ag 47 107.87

Make sure to define a __repr__ so that the textual representation of your elements is human-readable.


In [ ]:
class Element(object):
    def __init__(self, name, symbol, atomic_number, molecular_weight):
        self.name = name
        self.symbol = symbol,
        self.atomic_number = atomic_number
        self.molecular_weight = molecular_weight
        
    def __repr__(self):
        return "<Element " + self.name + ">"

Now create a new class that inherits the Element class, a SortableElement. It should implement the __lt__ and __eq__ and magic functions described here.


In [ ]:
class SortableElement(Element):
    
    def __lt__(self, another):
        return self.molecular_weight < another.molecular_weight
    
    def __eq__(self, another):
        return self.symbol == other.symbol

Make a list of SortableElements and sort it using list.sort() to try out your list.


In [ ]:
elements = [
    SortableElement("Coal", "C", 14, 14.0),
    SortableElement("Hydrogen", "H", 1, 1.008),
    SortableElement("Helium", "He", 2, 2)
]
elements.sort()
print(elements)
elements.sort(reverse=True)
print(elements)

Extra: Compound-class

Create a class compound, that will consist of multiple Element objects. Ignore physical restrictions on forming compounds for now.

Remember that an element can be present multiple times in a compound the way there are two Hydrogens in each water molecule.

Implement a get_molecular_weight for the compound.

If you have time and energy, you can implement addition of new elements, combining of compounds.


In [ ]:
#User beware, the code was written as an example but has not been tested.
class Compound(object):
    def __init__(self, name="Compound X"):
        self.elements = []
        self.element_counts = {}
        
    def add_element(self, element):
        if element not in self.elements:
            self.elements.append(element)
        if element not in self.element_counts:
            self.element_counts[element.name] = 1
        else:
            self.element_counts[element.name] += 1

    def get_molecular_weight(self):
        weight = 0
        for element in self.elements:
            weight += self.element_counts[element.name]*element.molecular_weight

Double extra: more Exceptions

Consider the following method, it will raise errors randomly. This type of failure is pretty common for IO-related tasks.


In [ ]:
class RandomException(Exception):
    pass
    

def do_wonky_stuff():
    import random
    if random.random() > 0.5:
        raise RandomException("this exception happened randomly")
    return

Wrap a call to do_wonky_stuff with a try-except clause.


In [ ]:
try:
    do_wonky_stuff()
except RandomException as ex:
    #just ignoring for now
    pass
print("yay it worked")

OK, now let's go even deeper.


In [ ]:
class ReallyRandomException(Exception):
    pass

def do_really_wonky_stuff():
    import random
    val = random.random()
    if val > 0.75:
        raise RandomException("this exception happened randomly")
    elif val < 0.15:
        raise ReallyRandomException("This exception is actually quite rare")
    return

Wrap do_really_wonky_stuff in a try-except -clause with two excepts. In the rarer of the excepts print out something so you'll if it's your lucky day.

In real life you'd probably want to handle different errors in a different way, or at least log or inform the user of what caused the error.


In [ ]:
try:
    do_really_wonky_stuff()
except RandomException:
    pass #again just ignore
except ReallyRandomException:
    print("It's your lucky day")

In [ ]: